package org.gicentre.apps.hide;

import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ComponentEvent;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.PrintWriter;

import javax.swing.filechooser.FileFilter;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.tree.TreeNode;

import org.gicentre.apps.hide.TreemapState.AppearanceType;
import org.gicentre.data.FieldType;
import org.gicentre.data.Data.FileType;
import org.gicentre.data.summary.SummariseField;
import org.gicentre.data.summary.SummariseNode;
import org.gicentre.data.summary.SummariseNull;
import org.gicentre.hive.Expression;
import org.gicentre.hive.ExpressionNotSupportedException;
import org.gicentre.hive.Type;
import org.gicentre.tweet.Tweeter;
import org.gicentre.utils.gui.BusyIcon;
import org.gicentre.utils.gui.HelpScreen;

import com.google.zxing.BinaryBitmap;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.NotFoundException;
import com.google.zxing.Result;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;

import processing.core.PApplet;
import processing.core.PFont;
import processing.core.PImage;
import processing.video.Capture;
import sun.security.x509.AVA;

/**HiDE
 * Software application for exploring datasets through hierarchical
 * layouts
 * @author Aidan Slingsby, giCentre
 *
 */

public class HideApp extends PApplet implements ClipboardOwner{

	private static final long serialVersionUID = -5883429178827883301L;
	private static final String versionNumber="2.1.8";
	PFont font; //font
	TreemapStateGui treemapStateGui; //dual role of graphic state and GUI
	TreemapPanel treemapPanel; //draws the graphic
	DecimalFormat df=new DecimalFormat("0.00");
	Tweeter tweeter; //enables tweeting
	HideDataLoader dataLoader=null;
	
	//tooltip replacement
	String tooltipLabel=null;
	String tooltipData=null;
	
	//webcam
	Capture capture; 
	boolean captureMode=false;
	
	HelpScreen helpScreen;
	private boolean showHive =false;
	
	JFileChooser fileDataDialog,fileStateDialog;
	
	boolean isApplet=false;//if it is, disable the camera
	boolean disableLoadfile=false;
	boolean flagToOpenFileDialog=false;
	
	int oldW,oldH; //old width and height
	
	boolean showGui=true;
	BusyIcon busyIcon;
	
	int framesAfterLoop=0;
	
	List<SavedState> savedStates=new ArrayList<SavedState>();
	int savedStateButtonW=60;
	
	//Need to register your application with Twitter, and enter the
	//keys there supply below.
	String customerKey = "REPLACE_WITH_YOUR_CUSTOMER_KEY";
	String customerSecret = "REPLACE_WITH_YOUR_CUSTOMER_SECRET";
	String bitlyUser="REPLACE_WITH_YOUR_BITLY_USERNAME";
	String bitlyKey="REPLACE_WITH_YOUR_BITLY_APPLICATION KEY";
//	String customerKey = "1ZQrwH87g8hpIfm5F7eA";
//	String customerSecret = "NIC3tLVVclgIXceTScxn8BUVuGx2vSYp1n2RXw2t4E";
//	String bitlyUser="sbbb717";
//	String bitlyKey="R_6e85ccd0f32fc480ecd96608480052cb";


	/**Main method
	 * 
	 * @param args
	 */
	public static void main(String[] args){
		PApplet.main(new String[]{"org.gicentre.apps.hide.HideApp"});
	}
	
	/** Initialises the application
	 * 
	 */
	public void setup(){
		println("HiDE: Hierarchical Data Explorer - v"+versionNumber);
		println("giCentre, City University London");
		println("http://gicentre.org/hide/");
		try{
			if (this.getParameter("applet").equals("true")){
				//then it's an applet
				isApplet=true;
			}
		}
		catch (NullPointerException e){
			//not an applet embedded in a web browser
		}
			
		
		if (frame!=null){
			frame.setResizable(true);
		}
		setWindowCaption("No data loaded");
		size(800,600);
//		smooth();
		oldW=-1;
		oldH=-1;
		
		//creates the font to use
		font=createFont("Tahoma",12); 


		if (isApplet && this.getParameter("datasetName")!=null){
			String filename=this.getParameter("datasetName");
			try{
				disableLoadfile=true;
				loadDataset(filename);
			}
			catch (IOException e) {
				System.err.println("Could not load file");
				System.exit(0);
			}
		}
		
		//creates the helpscreen
		helpScreen=new HelpScreen(this,font); 
		if (!isApplet){
			helpScreen.setHeader("HiDE v"+versionNumber, 22,18);
		}
		else{
			helpScreen.setHeader("HiDE v"+versionNumber+" (applet)", 22,18);
		}
		helpScreen.setFooter("http://gicentre.org/hide/", 16,12);
		helpScreen.putEntry("Click on +/-", "Add/remove level");
		helpScreen.putEntry("Click on \u25C7", "Swap adjacent levels");
		
		helpScreen.putEntry("Left/right click variable name", "Cycle back/forwards");
		helpScreen.putEntry("Drag variable name", "Choose value from menu");
		helpScreen.putEntry("l", "Toggle use local min for colour");
		helpScreen.putEntry("t", "Turn morph transitions on/off");
		helpScreen.putEntry("g", "Show/hide gui");
		helpScreen.addSpacer();
		if (!isApplet){
			helpScreen.putEntry("c", "Turn on camera (press again to capture)");
		}
		helpScreen.putEntry("CTRL-c", "Copy state (HiVE) to clipboard");
		helpScreen.putEntry("CTRL-v", "Paste state (HiVE) from clipboard");
		helpScreen.putEntry(":", "Tweet");
		helpScreen.putEntry("+", "Save state (bottom of screen)");
		helpScreen.putEntry("- (with mouse on state)", "Delete state");
		helpScreen.putEntry("Click state", "Restore state");
		helpScreen.putEntry("LEFT/RIGHT", "Go backward/forwards through saved states");
		helpScreen.putEntry("1-9", "Restore states (1=leftmost; 9=ninth from left)");
		if (!isApplet){
			helpScreen.putEntry("e", "Export image of graphic");
		}
		helpScreen.addSpacer();
		helpScreen.putEntry("l", "Toggle use local min for colour");
		helpScreen.putEntry("v", "Show HiVE state on screen");
		if (!disableLoadfile){
			helpScreen.putEntry("d", "Load another dataset");
			helpScreen.putEntry("r", "Restore appearance state or states from file");
			helpScreen.putEntry("a", "Save appearance state tofile");
			helpScreen.putEntry("s", "Save whole state to file (*.state)");
			helpScreen.putEntry("S", "Save all state to file (*.states)");
		}
		helpScreen.addSpacer();
		helpScreen.putEntry("h", "Show/hide help");

		if (!disableLoadfile){
			//creates the file dialog window
			fileDataDialog=new JFileChooser();
			fileDataDialog.addChoosableFileFilter(new HideFileFilter());
			try{
				fileDataDialog.setCurrentDirectory(new File(new File(".").getCanonicalPath()));
			}
			catch(IOException e){
				System.err.println(e);
			}
			fileStateDialog=new JFileChooser();
			fileStateDialog.addChoosableFileFilter(new HideStateFilter());
			try{
				fileStateDialog.setCurrentDirectory(new File(new File(".").getCanonicalPath()));
			}
			catch(IOException e){
				System.err.println(e);
			}
		}


		busyIcon=new BusyIcon();
		
		//ensure window redraws when a window is resized
		this.addComponentListener(new java.awt.event.ComponentAdapter(){
			public void componentResized(ComponentEvent e){
				loop();
			}
		});
	}
	
	/**Loads a dataset
	 * assumes
	 *   - the datafile has a .tab extension
	 *   - the data files are tab delimited and the first line is ignored
	 *   - the XML descriptor file has the same base filename with the ".xml" extension
	 *   See http://gicentre.org/hide/fileformat.php for details  
	 * @param filename with a .tab extension
	 */
	private void loadDataset(String filename) throws IOException{
		File file = new File(filename);
		if (!file.exists() && file.isFile()){
			throw new IOException("File "+filename+" does not exist.");
		}
		if (!filename.endsWith(".tab")){
			throw new IOException("File "+filename+" must have a '.tab' extension.");
		}
		String xmlFilename = filename.substring(0,filename.length()-4)+".xml";

		//HideDataLoader does the work of parsing the input files
		dataLoader=new HideDataLoader(createInput(xmlFilename),createReader(filename),FileType.TAB_DELIMITED,true);
	}
	
	
	/** Draws the screen
	 * Overrides PApplets draw() 
	 */
	@Override
	public void draw(){
		
		//if a treemapPanel exists (there isn't if no data is loaded) and the app window
		//has changed size, change its size
		if (treemapPanel!=null && (oldW!=width || oldH!=height-32)){
			oldW=width;
			oldH=height-32;
			treemapPanel.setBounds(new Rectangle(0,16,width, height-32));
		}
		
		
		//white background
		background(255);
		
		textFont(font);
		textSize(12);
		noStroke();
		if (!showGui && treemapStateGui!=null){
			textAlign(LEFT, TOP);
			fill(255,200,200);
			float w=textWidth("Mouseover here for controls");
			rect(0,0,max(treemapStateGui.bounds.width,w),16);
			fill(120);
			text("Mouseover here for controls",0,0);
		}
		if (treemapPanel!=null && treemapPanel.buffers!=null && treemapPanel.buffers.size()>1){
			fill(255,200,200);
			rect(width-100,0,100,16);
			fill(120);
			textAlign(RIGHT, TOP);
			text("Replay morph",width,0);			
		}
		
		//if the data is loading or has just loaded
		if (dataLoader!=null){
			if (!dataLoader.isDone){
				busyIcon.draw(this, width/2, height-30, 30);
			}
			else{
				//create a new treemap state
				treemapStateGui = new TreemapStateGui(this,0,0,font,2,1,1,dataLoader.getAllowedHierVars(),dataLoader.getAllowedOrderVars(),dataLoader.getAllowedSizeVars(),dataLoader.getAllowedColourVars(),dataLoader.getLayouts());
				//create new treemap panel
				treemapPanel = new TreemapPanel(this,treemapStateGui,font,new Rectangle(0,16,width, height-32),dataLoader.getData(),dataLoader.getSummariseFields(),dataLoader.getColours(),dataLoader.getColourScalings());
				//create Tweeter
				tweeter=new Tweeter(this,font,15,treemapStateGui,customerKey,customerSecret,treemapPanel,bitlyUser,bitlyKey);
				tweeter.setHashtag(dataLoader.getDatasetName());
				tweeter.allowChangeOfAccount(true);
				setWindowCaption(dataLoader.getDatasetName());
				
				dataLoader=null;//get rid of the data loader object
			}
		}
		//if no dataset loaded, write message on screen
		else if (treemapPanel==null){
			fill(150);
			textFont(font);
			textSize(20);
			textAlign(CENTER);
			text("Press 'd' to load data",width/2,height/2-20);
			fill(200);
			text("See http://gicentre.org/hide/ or press 'h' for help",width/2,height/2+20);
		}


		
		if (dataLoader==null && treemapPanel!=null){
			if (captureMode){
				//if capturing through camera, display live feed
				capture.read();
				image(capture.get(),0,0,width,height);
				fill(255);
				noStroke();
				rect(0,height-50,width,50);
				fill(150);
				textFont(font);
				textSize(20);
				textAlign(CENTER,CENTER);
				text("Press 'c' again to capture and attempt to process as QR code.",width/2,height-25);
			}
			else{
				//otherwise draw the graphic

				//animation
				if (mouseY<16 && mouseX>width-100 && treemapPanel!=null && treemapPanel.buffers!=null && treemapPanel.buffers.size()>1){
					image(treemapPanel.buffers.get((int)map(mouseX,width-100,width,0,treemapPanel.buffers.size()-1)),treemapPanel.bounds.x,treemapPanel.bounds.y);
					stroke(80,150);
					noFill();
					rect(mouseX-8,0,16,16);
				}
				else{
					treemapPanel.draw();
				}
				
				boolean panelIsBusy=false;
				if (treemapPanel.isBusy()){
					busyIcon.draw(this, width/2,height-30, 30);
					fill(0,100);
					textSize(16);
					textAlign(CENTER,CENTER);
					text("Press space to draw now",width/2,height-30);
					panelIsBusy=true;
				}
				
				//draw saved state buttons
				{
					textSize(12);
					textAlign(LEFT,CENTER);
					int x=0; 
					int y=(int)treemapPanel.bounds.getMaxY();
					noStroke();
					for (SavedState savedState:savedStates){
						if (treemapStateGui.stateEqualTo(savedState.treemapState))
							fill(200,100,100);
						else if (mouseY>y && mouseX>x && mouseX<x+savedStateButtonW)
							fill(200,180,180);
						else
							fill(200);
						rect(x,y,savedStateButtonW,16);
						fill(0,200);
						if (savedState.name!=null){
							text(savedState.name,x,y+6);
							x+=savedStateButtonW+2;
						}
						//redo thumbnail if this state is saved
						if (treemapStateGui.stateEqualTo(savedState.treemapState)){
							PImage image=null;
							try{
								image=(PImage)treemapPanel.getImageToTweet().clone();
							}
							catch (CloneNotSupportedException e) {

							}
							float imageRatio=image.width/(float)image.height;
							image.resize(200,(int)(200/imageRatio));
							savedState.thumbnail=image;
						}

						
					}
				}
				
				if (treemapStateGui.getHierarchyFields().length>0 && treemapPanel.treemap==null){
					fill(150);
					textFont(font);
					textSize(20);
					textAlign(CENTER,CENTER);
					text("No data to display.\n\nTry removing some of the filters.",width/2,height/2);				 	
				}
				
				if (showGui && !tweeter.isActive()){
					treemapStateGui.draw();
				}

				if (treemapStateGui.getHierarchyFields().length==0){
					fill(150);
					textFont(font);
					textSize(20);
					textAlign(CENTER,CENTER);
					text("Click the + symbol above to start showing data.\n\nPress 'h' for help.",width/2,height/2);
				}

				if (tooltipLabel!=null && !tweeter.isActive()){
					
					textSize(12);
					//work out w and h
					int w=(int)textWidth(tooltipLabel)+2;
					int h=14;
					if (tooltipData!=null){
						String toks[]=tooltipData.split("\\\n");
						h+=toks.length*11;
						for (String tok:toks){
							textSize(10);
							int localW=(int)textWidth(tok);
							if (localW>w){
								w=localW;
							}
						}
					}
					
					stroke(80,20);
					textAlign(LEFT,TOP);
					int x=mouseX+10;
					if (x+w>width){
						x=width-w;
					}
					int y=mouseY;
					if (y+h>height){
						y=height-h;
					}
					fill(255,248,147,200);
					rect(x,mouseY,w,h);
					fill(80);
					textSize(12);
					text(tooltipLabel,x,mouseY);
					fill(100);
					textSize(10);
					textLeading(11);
					text(tooltipData,x,mouseY+14);
				}


				//this will only draw if it's active
				tweeter.draw();

				if (showHive){
					textAlign(CENTER,BOTTOM);
					textSize(20);
					fill(50);
					if (!treemapStateGui.isEmpty()){
						text(Expression.formatExpression(treemapStateGui.getState()),0,0,width,(int)treemapPanel.getBounds().getMaxY());
					}
					else{
						text("",0,0,width,height);						
					}
				}
				
				//only stop looping (drawing) if not in transition and not capturing
				if (framesAfterLoop>20 && !captureMode && !treemapPanel.isInTransition() && !panelIsBusy){
					noLoop();
				}
				else
					framesAfterLoop++;
				
			}

			
		}
		
		//display image if mouse is over a state
		if (treemapPanel!=null && mouseY>treemapPanel.bounds.getMaxY()){
			int x=0; 
			for (SavedState savedState:savedStates){
					x+=savedStateButtonW+2;
					if (savedState.thumbnail!=null){
					if (mouseX<x){
						int xDisp=x-savedStateButtonW/2-savedState.thumbnail.width/2;
						if (xDisp<0)
							xDisp=0;
						int yDisp=(int)treemapPanel.bounds.getMaxY()-savedState.thumbnail.height;
						fill(255,20);
						noStroke();
						for (int i=20;i>0;i-=2)
							rect(xDisp-i,yDisp-i,savedState.thumbnail.width+i*2,savedState.thumbnail.height+i);
						image(savedState.thumbnail,xDisp,yDisp);
						break;
					}
				}
			}
		}
		
		//draw helpscreen
		if (helpScreen.getIsActive()){
			helpScreen.draw();
		}
		
		if (flagToOpenFileDialog){
			flagToOpenFileDialog=false;
			if (fileDataDialog.showOpenDialog(this)==JFileChooser.APPROVE_OPTION){
				try{
					loadDataset(fileDataDialog.getSelectedFile().toString());
				}
				catch (IOException e){
					System.err.println("Error loading the file");
				}
			}
		}

		
	}
	
	public void loop(){
		super.loop();
		framesAfterLoop=0;
	}
	
	private void setWindowCaption(String datasetName){
		if (frame!=null){
			if (datasetName==null){
				frame.setTitle("HiDE v"+versionNumber+": Hierarchical Data Explorer");
			}
			else{
				frame.setTitle("HiDE v"+versionNumber+": Hierarchical Data Explorer ["+datasetName+"]");
			}
		}
	}
	
	private void restoreState(String hive){

		//store appearance - to restore later
		HashMap<AppearanceType, Double>[] oldAppearance = new HashMap[treemapStateGui.getNumLevels()];
		for (int i=0;i<oldAppearance.length;i++){
			oldAppearance[i]=new HashMap<TreemapState.AppearanceType, Double>();
			for (AppearanceType appearanceType:AppearanceType.values()){
				oldAppearance[i].put(appearanceType,treemapStateGui.getAppearanceValue(appearanceType, i));
			}
		}
		
		treemapStateGui.clear();
		Collection<Expression> expressions=Expression.parseExpressions(hive);
		//do hier first
		for (Expression expression : expressions){
			if (expression.getType().equals(Type.S_HIER) || expression.getType().equals(Type.O_HIER)){
				try{
					treemapStateGui.applyOperator(expression);
				}
				catch (ExpressionNotSupportedException e){
					System.err.println(e);
				}
			}
		}
		//then do layouts
		for (Expression expression : expressions){
			if (expression.getType().equals(Type.S_LAYOUT) || expression.getType().equals(Type.O_LAYOUT)){
				try{
					treemapStateGui.applyOperator(expression);
				}
				catch (ExpressionNotSupportedException e){
					System.err.println(e);
				}
			}
		}
		//then do everything else except focus
		for (Expression expression : expressions){
			if (!expression.getType().equals(Type.S_HIER) && !expression.getType().equals(Type.O_HIER) &&
					!expression.getType().equals(Type.S_FOCUS) && !expression.getType().equals(Type.O_FOCUS) &&
					!expression.getType().equals(Type.S_LAYOUT) && !expression.getType().equals(Type.O_LAYOUT)){
				try{
					treemapStateGui.applyOperator(expression);
				}
				catch (ExpressionNotSupportedException e){
					System.err.println(e);
				}
			}
		}
		//finally, do focus
		for (Expression expression : expressions){
			if (expression.getType().equals(Type.S_FOCUS) || expression.getType().equals(Type.O_FOCUS)){
				try{
					treemapStateGui.applyOperator(expression);
				}
				catch (ExpressionNotSupportedException e){
					System.err.println(e);
				}
			}
		}
		
		//then restore appearance
		//store appearance - to restore later
		if (oldAppearance.length>0){
			for (int i=0;i<min(oldAppearance.length,treemapStateGui.getNumLevels());i++){
				for (AppearanceType appearanceType:oldAppearance[i].keySet()){
					treemapStateGui.setAppearanceValue(i, appearanceType,oldAppearance[i].get(appearanceType));
				}
			}
		}
		treemapPanel.flagToDoStructuralRebuild();
		System.out.println("Restored state to "+hive);
	
	}
	
	public void keyPressed(){
		
		if (tweeter!=null){
			//must do this first - also deals with activation etc
			tweeter.keyPressed();//if key is processed, the applet.key is reset (set to 0)
		}
				
		//loading a new dataset
		if (!disableLoadfile && !captureMode && (key=='d'||key=='D')){
			flagToOpenFileDialog=true;
		}

		if (key=='e'||key=='E'){
			DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
			String filename="screenshot_"+dateFormat.format(new Date())+".png";
			PImage image = treemapPanel.getImageToTweet();
			image.save(filename);
		}

		
		//copying state to clipboard
		if (treemapPanel!=null && (keyEvent.isControlDown()||keyEvent.isMetaDown()) && (key=='c'||key=='C'||keyCode+32=='c'||keyCode+32=='C')){ //keyCode+32 because this is how you find the key if CTRL is pressed 
			if (!treemapStateGui.isEmpty()){
				Clipboard clipboard=Toolkit.getDefaultToolkit().getSystemClipboard();
				clipboard.setContents(new StringSelection(Expression.formatExpression(treemapStateGui.getState())),this);
				System.out.println("Copied state to clipboard");
			}
			else{
				System.out.println("No state to copy");
			}
		}
		//restoring (pasting) a state from the clipboard
		if (treemapPanel!=null && (keyEvent.isControlDown()||keyEvent.isMetaDown()) && (key=='v'||key=='V'||keyCode+32=='v'||keyCode+32=='V')&&!tweeter.isActive()){		 //keyCode+32 because this is how you find the key if CTRL is pressed
			String state=null;
			try {
				Clipboard clipboard=Toolkit.getDefaultToolkit().getSystemClipboard();
				state=clipboard.getContents(this).getTransferData(DataFlavor.stringFlavor).toString();
			} catch (UnsupportedFlavorException e) {
				System.err.println(e);
			} catch (IOException e) {
				System.err.println(e);
			}
			if (state!=null){
				restoreState(state);
			}

		}
		
		//restore a state from a QR code in chart.png
		
//		if (key=='r'){
//			restoreGraphicFromQR(loadImage("/Users/sbbb717/Desktop/chart.png").getImage());
//		}
		
		//turning on the camera
		if (!isApplet){
			if (treemapPanel!=null && !keyEvent.isControlDown() && !keyEvent.isMetaDown() && (key=='c'||key=='C') && !captureMode && treemapPanel!=null){
				if (capture==null){
					capture = new Capture(this,1000,1000);
				}
				captureMode=true;
			}
			//restoring state from HiVE from QR code (second time 'c' is pressed) 
			else if (treemapPanel!=null && !keyEvent.isControlDown() && !keyEvent.isMetaDown() && (key=='c'||key=='C') && captureMode){
				//try and read
				captureMode=!restoreGraphicFromQR(capture.getImage());
				captureMode=false;
			}
		}

		//local colouring
		if (treemapPanel!=null && key=='l' || key=='L'){
			treemapPanel.setUseLocalMin(!treemapPanel.getUseLocalMin());
			treemapPanel.flagToRedraw();
		}

		//show/hide GUI
		if (treemapPanel!=null && key=='g' || key=='G'){
			showGui=!showGui;
//			oldH=oldH+1;//just make it look like the window has resized
		}
		
		//show/hide help screen
		if (key=='h'||key=='H'){
			helpScreen.setIsActive(!helpScreen.getIsActive());
		}
		
		//show/hide HiVE expression
		if (treemapPanel!=null && !keyEvent.isControlDown() && !keyEvent.isMetaDown() && key=='v'||key=='V'){
			showHive=!showHive;
		}
		
		if (treemapPanel!=null && key=='t' || key=='T'){
			treemapPanel.setTransitionsOn(!treemapPanel.isTransitionsOn());
		}
		//load appearance
		if (!disableLoadfile && key=='r' && treemapStateGui!=null){
			fileStateDialog.setDialogTitle("Load the appearance state");
			if (fileStateDialog.showOpenDialog(this)== JFileChooser.APPROVE_OPTION){
				try{
					if (fileStateDialog.getSelectedFile().toString().endsWith(".state")){
						treemapStateGui.inputStateFromFile(createReader((fileStateDialog.getSelectedFile().toString())));
					}
					else if (fileStateDialog.getSelectedFile().toString().endsWith(".states")){
						String fileContents="";
						String stateName=null;
						BufferedReader br=createReader(fileStateDialog.getSelectedFile().toString());
						while (br.ready()){
							String line = br.readLine();{
								if (line.startsWith("State\t")||!br.ready()){
									if (fileContents!=null){
										TreemapState newTreemapState = new TreemapState(2,1,1,treemapStateGui.getAllowedHierarchyFields(),treemapStateGui.getAllowedOrderFields(),treemapStateGui.getAllowedSizeFields(),treemapStateGui.getAllowedColourFields(),treemapStateGui.getAllowedLayouts());
										newTreemapState.inputStateFromStatefileContents(fileContents);
										SavedState savedState=new SavedState(newTreemapState);
										if (savedState.treemapState.getNumLevels()>0){
											savedStates.add(savedState);
											savedState.name=stateName;
										}
									}
									stateName=line.split("\t")[1];
									fileContents="";

								}
								fileContents+=line+"\n";
							}
						}
					}
				}
				catch (IOException e){
					System.err.println("Error restoring the state");
				}
			}
		}		
		//redraw
		//save appearance
		if (!disableLoadfile && key=='a'){
			fileStateDialog.setDialogTitle("Save the appearance state");
			if (fileStateDialog.showSaveDialog(this)== JFileChooser.APPROVE_OPTION){
				if (fileStateDialog.getSelectedFile().exists()) {
					int response = JOptionPane.showConfirmDialog (null,"Overwrite existing file?","Confirm Overwrite",JOptionPane.OK_CANCEL_OPTION,JOptionPane.QUESTION_MESSAGE);
					if (response == JOptionPane.CANCEL_OPTION) return;
				}
				String filename=fileStateDialog.getSelectedFile().toString();
				
				if (!filename.endsWith(".state")){
					filename+=".state";
				}
				try{
					treemapStateGui.saveStateToFile(filename,true);
				}
				catch (IOException e){
					System.err.println("Error saving the state");
				}
			}
		}		
		//save hive AND appearance
		if (!disableLoadfile && key=='s'){
			fileStateDialog.setDialogTitle("Save the state");
			if (fileStateDialog.showSaveDialog(this)== JFileChooser.APPROVE_OPTION){
				if (fileStateDialog.getSelectedFile().exists()) {
					int response = JOptionPane.showConfirmDialog (null,"Overwrite existing file?","Confirm Overwrite",JOptionPane.OK_CANCEL_OPTION,JOptionPane.QUESTION_MESSAGE);
					if (response == JOptionPane.CANCEL_OPTION) return;
				}
				String filename=fileStateDialog.getSelectedFile().toString();
				if (!filename.endsWith(".state")){
					filename+=".state";
				}
				try{
					treemapStateGui.saveStateToFile(filename,false);
				}
				catch (IOException e){
					System.err.println("Error saving the state");
				}
			}
		}		
		if (!disableLoadfile && key=='S'){
			fileStateDialog.setDialogTitle("Save all the states");
			if (fileStateDialog.showSaveDialog(this)== JFileChooser.APPROVE_OPTION){
				if (fileStateDialog.getSelectedFile().exists()) {
					int response = JOptionPane.showConfirmDialog (null,"Overwrite existing file?","Confirm Overwrite",JOptionPane.OK_CANCEL_OPTION,JOptionPane.QUESTION_MESSAGE);
					if (response == JOptionPane.CANCEL_OPTION) return;
				}
				String filename=fileStateDialog.getSelectedFile().toString();
				if (!filename.endsWith(".states")){
					filename+=".states";
				}
				try{
					BufferedWriter bw = new BufferedWriter(new PrintWriter(filename));
					bw.write("//Appearance state for HiDE, written on "+new SimpleDateFormat("yyyy MM dd hh:00").format(GregorianCalendar.getInstance().getTime())+" by "+System.getProperty("user.name")+"\n");
					for (SavedState savedState:savedStates){
						bw.write("\nState\t"+savedState.name+"\n");
						bw.write(savedState.treemapState.getSavedStateTextRepresention(false));
					}
					bw.close();
					System.out.println("Saved appearance to "+filename);
				}
				catch (IOException e){
					System.err.println("Error saving the state");
				}
			}
		}		
		if (key=='+' || key=='='){
			if (treemapPanel!=null && treemapStateGui.getNumLevels()>0 && getSavedStateForCurrentState()==null){
				SavedState savedState=treemapStateGui.getSavedState();
				PImage image=null;
				try{
					image=(PImage)treemapPanel.getImageToTweet().clone();
				}
				catch (CloneNotSupportedException e) {

				}
				float imageRatio=image.width/(float)image.height;
				image.resize(200,(int)(200/imageRatio));
				savedState.thumbnail=image;
				int maxStateNumber=-1;
				Pattern pattern=Pattern.compile("^state([0-9]+)$");
				for (SavedState savedState2:savedStates){
					Matcher matcher=pattern.matcher(savedState2.name);
					if (matcher.matches()){
						int stateNumber=Integer.parseInt(matcher.group(1));
						if (stateNumber>maxStateNumber)
							maxStateNumber=stateNumber;
					}
				}
				savedState.name="state"+(maxStateNumber+1);
				savedStates.add(savedState);
			}
		}
		if (key=='-'){
			int x=0;
			for (SavedState savedState:savedStates){
				x+=savedStateButtonW+2;
				if (mouseX<x){
					savedStates.remove(savedState);
					break;
				}
			}
		}
		if (key==CODED && keyCode==LEFT){
			SavedState savedState=getSavedStateForCurrentState();
			int idx=savedStates.indexOf(savedState);
			idx--;
			if (idx<0)
				idx=savedStates.size()-1;
			treemapStateGui.setState(savedStates.get(idx).treemapState,false);
		}
		if (key==CODED && keyCode==RIGHT){
			SavedState savedState=getSavedStateForCurrentState();
			int idx=savedStates.indexOf(savedState);
			if (idx==-1)
				idx=9999;
			idx++;
			if (idx>savedStates.size()-1)
				idx=0;
			treemapStateGui.setState(savedStates.get(idx).treemapState,false);
		}
		if (key=='1' && savedStates.size()>=1)
			treemapStateGui.setState(savedStates.get(0).treemapState,false);			
		if (key=='2' && savedStates.size()>=2)
			treemapStateGui.setState(savedStates.get(1).treemapState,false);			
		if (key=='3' && savedStates.size()>=3)
			treemapStateGui.setState(savedStates.get(2).treemapState,false);			
		if (key=='4' && savedStates.size()>=4)
			treemapStateGui.setState(savedStates.get(3).treemapState,false);			
		if (key=='5' && savedStates.size()>=5)
			treemapStateGui.setState(savedStates.get(4).treemapState,false);			
		if (key=='6' && savedStates.size()>=6)
			treemapStateGui.setState(savedStates.get(5).treemapState,false);			
		if (key=='7' && savedStates.size()>=7)
			treemapStateGui.setState(savedStates.get(6).treemapState,false);			
		if (key=='8' && savedStates.size()>=8)
			treemapStateGui.setState(savedStates.get(7).treemapState,false);			
		if (key=='9' && savedStates.size()>=9)
			treemapStateGui.setState(savedStates.get(8).treemapState,false);			
		if (key==' '){
			treemapPanel.cancelRendering();
		}
		if (key == ESC) key=0; // trap ESC so it doesn't quit
		
		loop();
	}
	
	private SavedState getSavedStateForCurrentState(){
		for (int i=0;i<savedStates.size();i++)
			if (treemapStateGui.stateEqualTo(savedStates.get(i).treemapState))
				return savedStates.get(i);
		return null;
	}
	
	public void keyReleased(){
		loop();
	}
	
	/** Attempts to restore a graphic from a QR code using zxing libraries
	 * 
	 * @param image containing QR code
	 * @return
	 */
	private boolean restoreGraphicFromQR(Image image){
		BufferedImage bufferedImage = new BufferedImage(image.getWidth(frame), image.getHeight(frame), BufferedImage.TYPE_INT_ARGB_PRE);
		Graphics2D g2d = bufferedImage.createGraphics();
		g2d.drawImage(image, 0, 0, image.getWidth(frame), image.getHeight(frame), this);
		//make b/w
		LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
		BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
		try {
			MultiFormatReader multiFormatReader = new MultiFormatReader();
			//extract HiVE if possible
			Result result = multiFormatReader.decode(bitmap);
			String state = result.getText();
			if (state!=null){
				restoreState(state);
			}
		} catch (NotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return false;
		}
		return true;
	}

	public void mouseDragged(){
		treemapStateGui.mouseDragged();
		loop();
	}

	public void mouseReleased(){
		if (treemapStateGui!=null)
			treemapStateGui.mouseReleased();
		loop();
	}

	
	
	/**
	 * Mousemoved event
	 */
	public void mouseMoved(){
		
		//show/hide gui
		if ((!showGui && mouseY<16 && treemapStateGui!=null && mouseX<treemapStateGui.getBounds().getWidth()) ||
				(showGui && treemapStateGui!=null && treemapStateGui.getBounds().contains(mouseX,mouseY))){
			showGui=true;
		}
		else{
			if (treemapStateGui!=null && treemapStateGui.getNumLevels()>0)
				showGui=false;
		}

		
		//redraw everytime the mouse is moved
		loop();
		tooltipLabel=null;
		tooltipData=null;
		if (treemapPanel!=null && (!showGui || (showGui&&!treemapStateGui.contains(mouseX,mouseY)))){
			//identify the node (rectangle) that the mouse is over
			TreemapSummaryNode node = treemapPanel.getNodeFromMouse();
			
			if (node!=null){
				tooltipLabel="";
				TreeNode[] treenodes=node.getSummariseNode().getPath();

				//construct the path through the hierarchy to the path
				for (int i=1;i<treenodes.length;i++){
					String thisLabel=((SummariseNode)treenodes[i]).getConditioningValueAsString();
						if (thisLabel==null){
							thisLabel="<no value>";
						}
						else{
							thisLabel=thisLabel.replaceAll("\\_", " ");
						}
						tooltipLabel+=thisLabel;
					if (i<treenodes.length-1){
						tooltipLabel+=" > ";
					}
				}
				tooltipData="";
				//establish the summary variable values shown as part of the state 
				HashSet<SummariseField> summaryFieldsToDisplay=new HashSet<SummariseField>();
				//sizes
				for (int i=0;i<treemapStateGui.getSizeFields().length;i++){
					summaryFieldsToDisplay.add(treemapStateGui.getSizeFields()[i][treemapStateGui.getSizeFields()[i].length-1]);
				}
				//orders
				for (int i=0;i<treemapStateGui.getOrderFields().length;i++){
					summaryFieldsToDisplay.add(treemapStateGui.getOrderFields()[i][treemapStateGui.getOrderFields()[i].length-1]);
				}
				//colours
				for (int i=0;i<treemapStateGui.getColourFields().length;i++){
					SummariseField summariseField=treemapStateGui.getColourFields()[i][treemapStateGui.getColourFields()[i].length-1];
					if (summariseField!=null && summariseField instanceof SummariseNull==false){
						summaryFieldsToDisplay.add(summariseField);
					}
				}
				summaryFieldsToDisplay.remove(null);

				//find the values of each
				for (SummariseField summariseField : summaryFieldsToDisplay){
					String value="";
					if (summariseField instanceof SummariseNull==false){
						if (summariseField.getFieldType()==FieldType.STRING){
							value=node.getSummariseNode().getSummaryAsString(summariseField);
						}
						if (summariseField.getFieldType()==FieldType.INT || summariseField.getFieldType()==FieldType.LONG){
							value=node.getSummariseNode().getSummaryAsLong(summariseField)+"";
						}						
						else{
							Double v=node.getSummariseNode().getSummaryAsDouble(summariseField);
							if (v!=null){
								if (Math.abs(v)>=1){
									value=df.format(v);
								}
								else{
									MathContext mc = new MathContext(2, RoundingMode.HALF_UP);
									BigDecimal bigDecimal = new BigDecimal(v, mc);
									value=bigDecimal.toPlainString();
								}
							}
						}
						tooltipData+=summariseField.getName()+"="+value;
						if (summariseField.getUnits()!=null){
							tooltipData+=" "+summariseField.getUnits();
						}
						tooltipData+="\n";
					}
				}
				int n=node.getSummariseNode().getRecords().size();
				if (n==1){
					tooltipData+=n+" record";
				}
				else{
					tooltipData+=n+" records";
				}
				//set the text on the tooltip and enable
			}
		}
	}
	
	/**Mouseclicked event
	 * 
	 */
	public void mouseClicked(){
		//check that the mouse click is relevant to the GUI
		if (treemapStateGui!=null && showGui){
			treemapStateGui.mouseClicked();
		}
		//check if mouse is on states
		if (treemapPanel!=null && mouseY>treemapPanel.bounds.getMaxY()){
			int x=0; 
			for (SavedState savedState:savedStates){
				x+=savedStateButtonW+2;
				if (mouseX<x){
					treemapStateGui.setState(savedState.treemapState,false);
					break;
				}
			}
		}

		
		//redraw
		loop();
	}

	
	public void lostOwnership(Clipboard arg0, Transferable arg1) {
		// TODO Auto-generated method stub
		
	}
	
	/**
	 * File filter for .tab file, where there's also a .xml file
	 */
	public class HideFileFilter extends FileFilter{

		@Override
		public boolean accept(File file){
			File parent = file.getParentFile();
			if (parent!=null){
				if (file.isDirectory()){
					return true;
				}
				else if (file.getAbsolutePath().endsWith(".tab")){
					String xmlFile =file.getAbsolutePath().toString().substring(0,file.getAbsoluteFile().toString().length()-4)+".xml";
					for(File sibling :parent.listFiles()){
						try{
							if (sibling.getCanonicalPath().equals(xmlFile)){
								return true;
							}
						}
						catch (IOException e){
						}
					}
				}
			}
			return false;
		}

		@Override
		public String getDescription() {
			return "HiDE data (need .tab and .xml)";
		}
	}
	
	/**
	 * File filter for .tab file, where there's also a .xml file
	 */
	public class HideStateFilter extends FileFilter{

		@Override
		public boolean accept(File file){
			File parent = file.getParentFile();
			if (parent!=null){
				if (file.isDirectory()||file.getAbsolutePath().endsWith(".state")||file.getAbsolutePath().endsWith(".states")){
					return true;
				}
			}
			return false;
		}

		@Override
		public String getDescription() {
			return "HiDE state";
		}
	}

	
}
